Skip to content

fix(dav): return RFC 4791 no-uid-conflict on duplicate calendar UID#61640

Closed
ndo84bw wants to merge 1 commit into
nextcloud:masterfrom
ndo84bw:fix/caldav-no-uid-conflict
Closed

fix(dav): return RFC 4791 no-uid-conflict on duplicate calendar UID#61640
ndo84bw wants to merge 1 commit into
nextcloud:masterfrom
ndo84bw:fix/caldav-no-uid-conflict

Conversation

@ndo84bw

@ndo84bw ndo84bw commented Jun 29, 2026

Copy link
Copy Markdown

Summary

If a client attempts to create (PUT) a calendar object whose UID already exists in the target calendar, NC currently responds with a generic 400 Bad Request ("Calendar object with uid already exists in this calendar collection."). RFC 4791 specifies a separate precondition for this exact case. This PR implements it:

CalDavBackend::createCalendarObject now throws OCA\DAV\CalDAV\Exception\UidConflict (inherits from Sabre\DAV\Exception\Conflict -> 409), which satisfies the RFC 4791 precondition (Section 5.3.2.1) CALDAV:no-uid-conflict with a DAV:href pointing to the existing object:

<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
  <s:exception>OCA\DAV\CalDAV\Exception\UidConflict</s:exception>
  <s:message>Calendar object with uid already exists in this calendar collection.</s:message>
  <cal:no-uid-conflict xmlns:cal="urn:ietf:params:xml:ns:caldav">
    <d:href xmlns:d="DAV:">/remote.php/dav/calendars/USER/personal/EXISTING.ics</d:href>
  </cal:no-uid-conflict>
</d:error>

This ensures the server is spec-compliant and provides the client with a machine-readable reference to the conflicting object, allowing the client to correct itself (by writing to the href or synchronizing).

Status 409 instead of 403: RFC 4791 Section 1.3 - the client CAN resolve the conflict and resend, hence 409 (not 403).

Background / Motivation

If you accept an email event invitation from NC in Thunderbird and try to add it to the calendar of the same NC instance as the organizer before Thunderbird has synchronized its calendar with NC, Thunderbird will attempt to PUT the event using a UUID that already exists on the server.
This is a client issue (Thunderbird should sync before accepting) and is known as: Thunderbird Bug 1717401 ("Accepting invitation before calendar sync fails (Status 80004005)"). This PR does not resolve this issue in NC. It merely makes the server response more precise - providing a clean basis for a client (TB) to respond to.

Testing

Manuell live (CalDAV-PUT gegen eine laufende Instanz):

  • Created an event in the web interface and saved the ICS file via export. Sent a PUT request to the same calendar with this ICS file using Curl and checked the response. -> 409 Conflict with cal:no-uid-conflict + d:href
$ curl -s -w "\n--> %{http_code}\n" -u admin:admin -X PUT -H "Content-Type: text/calendar; charset=utf-8" --data-binary @event2.ics http://nextcloud-dev:8080/remote.php/dav/calendars/admin/personal/deposited.ics
<?xml version="1.0" encoding="utf-8"?>
<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
  <s:exception>OCA\DAV\CalDAV\Exception\UidConflict</s:exception>
  <s:message>Calendar object with uid already exists in this calendar collection.</s:message>
  <cal:no-uid-conflict xmlns:cal="urn:ietf:params:xml:ns:caldav">
    <d:href xmlns:d="DAV:">/remote.php/dav/calendars/admin/personal/8242054B-8CD2-42C9-BD92-543C5FF5BC0F.ics</d:href>
  </cal:no-uid-conflict>
</d:error>

--> 409
  • Confirmed with the real Thunderbird (152): TB now receive a 409 UidConflict with no-uid-conflict/href during "accept-before-sync" instead of the generic 400 (see video)
duplicate-uuid-nc-small.mp4

Duplicate-UID behavior across CalDAV servers

Server Status Body
Nextcloud (before this PR) 400 Sabre text "…uid already exists…"
Nextcloud (with this PR) 409 CALDAV:no-uid-conflict + DAV:href
Radicale 3.7.5 409 CALDAV:no-uid-conflict (no href)
SOGo v5 Demo (2026-06-29) 409 DAV:error "Event UID already in use"

Note

Changing the exception class affects one catch site outside the diff: CalendarImpl::createFromStringInServer (catch (Conflict)) now wraps the duplicate-UID case into the contractual CalendarException instead of letting the former BadRequest escape uncaught. No other catch sites are affected.

TODO

  • Accept the new RFC interpretation (409 vs 403 instead 400)
  • Review the code

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI
  • Suggested code changes to me
  • Wrote the unit test

Assisted-by: ClaudeCode:claude-opus-4-8[1m]
Signed-off-by: Nico Donath <ndo84bw@gmx.de>
@ndo84bw ndo84bw requested review from leftybournes, nfebe, salmart-dev and sorbaugh and removed request for a team June 29, 2026 06:23
@ChristophWurst

Copy link
Copy Markdown
Member

Thanks for the patch

What is the difference to #61207?

@ChristophWurst ChristophWurst added 3. to review Waiting for reviews feature: dav feature: caldav Related to CalDAV internals bug labels Jun 29, 2026
@ChristophWurst ChristophWurst added this to the Nextcloud 35 milestone Jun 29, 2026
@ndo84bw

ndo84bw commented Jun 29, 2026

Copy link
Copy Markdown
Author

Hi @ChristophWurst,

The difference is the approach. #61207 tried to fix it in the server: it detected the duplicate UID and rewrote the client's request onto the existing object - the server guessing what the client meant. But the actual cause is on the client side: Thunderbird should re-fetch the calendar when it hits the conflict.

This PR deliberately does not do that. It only corrects the server's response: on a duplicate UID, NC currently returns a non-standard 400; with this PR it returns the RFC 4791 409 with CALDAV:no-uid-conflict (and a DAV:href to the existing object) - the same 409 that Radicale and SOGo already return.

I had hoped Thunderbird would then recover on its own, but it doesn't yet - so I'm also pushing for the client-side fix in Thunderbird (Bug 1717401). As long as NC answers with the non-standard 400, one side or the other needs a quirk. Once NC returns the standard 409 and Thunderbird acts on it (sync before accept/update), neither side needs a quirk.

@susnux susnux added the community pull requests from community label Jun 29, 2026

@SebastianKrupinski SebastianKrupinski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good approach!

Comment thread apps/dav/lib/CalDAV/CalDavBackend.php
@ChristophWurst ChristophWurst added 4. to release Ready to be released and/or waiting for tests to finish and removed 3. to review Waiting for reviews labels Jun 29, 2026
@ChristophWurst

Copy link
Copy Markdown
Member

/backport to stable34

@ChristophWurst

Copy link
Copy Markdown
Member

/backport to stable33

@ChristophWurst

Copy link
Copy Markdown
Member

/backport to stable32

@ChristophWurst

Copy link
Copy Markdown
Member

/backport to stable32

@SebastianKrupinski

Copy link
Copy Markdown
Contributor

@ChristophWurst

This will never pass CI, as its a fork, I will need to make a new PR from this.

@SebastianKrupinski

Copy link
Copy Markdown
Contributor

Hi @ndo84bw

Thank you again for the PR.

I am closing this PR one in favour of:

#61659

The reason is that our server CI will never pass on a forked branch, unlike the PR you have created in calendar before.

I also made some upgrades to your original PR, it was a good start, I just applied it to more then just calendar create.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4. to release Ready to be released and/or waiting for tests to finish backport-request bug community pull requests from community feature: caldav Related to CalDAV internals feature: dav

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants